#include "Graph.h"

#include <QDebug>
#include <QMouseEvent>

#include <QtAlgorithms>

#include <cmath>

//************************************
//
// RegLine
//
//************************************

RegLine::RegLine() :
    slope(0.0), intercept(0.0)
{

}

void RegLine::calcLinReg(Record::RecordField type,
                    QList<Record>::Iterator start,
                    QList<Record>::Iterator end)
{
    startTime = (*start).observationDate;
    endTime = (*end).observationDate;

    int recordCount = 0;
    double mx = 0, mxsq = 0, my = 0, mxy = 0;

    for (auto iter = start; iter < end; ++iter)
    {
        Record r = *iter;

        double x = startTime.secsTo(r.observationDate) / 3600.0;
        double y = r.getData(type);

        mx += x;
        mxsq += x * x;
        my += y;
        mxy += x * y;

        recordCount++;
    }

    if (recordCount < 1)
        return;

    mx = mx / recordCount;
    mxsq = mxsq / recordCount;
    my = my / recordCount;
    mxy = mxy / recordCount;

    slope = (mxy - mx * my) / (mxsq - mx * mx);
    intercept = my - slope * mx;

    unitString = Record::getUnitString(type);
}

double RegLine::getY(const QDateTime time)
{
    double deltaTime = startTime.secsTo(time) / 3600.0;

    return intercept + deltaTime * slope;
}

QString RegLine::getUnitString()
{
    return unitString;
}

//****************************************
//
// GraphDisplay
//
//****************************************

GraphDisplay::GraphDisplay(QWidget *parent) :
    QWidget(parent)
{
    isSelectingRange = false;
    selectStart = selectEnd = 0;

    rangeStart = QDateTime::currentDateTime();
    rangeEnd = QDateTime::currentDateTime();

    rangeYStart = 0;
    rangeYEnd = 100;
    rangeY = 100;

    intervalX = 50;
    intervalXMin = 50;
    intervalXScale = 1;
    intervalXOffset = 0;

    intervalY = 60;

    intervalType = INTERVAL_TYPE_HOURS;
    recordType = Record::RECORD_TEMP;

    valueMin = 0.0f;
    valueMax = 1.0f;

    marginBottom = 20;
    marginLeft = 0;
    marginRight = 0;
    marginTop = 0;

    gridVertPen = QPen(QColor::fromRgb(245, 245, 245), 1);
    gridHorizPen = QPen(QColor::fromRgb(180, 200, 220), 1);
    gridIntervalPen = QPen(Qt::black);

	graphPen = QPen(QColor::fromRgb(243, 45, 0));

    bottomBoxBrush = QBrush(QColor::fromRgb(140, 190, 250));
	graphBrush = QBrush(QColor::fromRgb(243, 45, 0, 80));

    graphArea = QRect(QPoint(marginLeft, marginTop),
                      QPoint(width() - marginRight, height() - marginBottom));

    scalingMarginY = 0.1f;

	cursorPositionX = 0;
    pixelsPerSec = 1;

    showRegressionLine = false;

    hoverRecord = nullptr;

    setMouseTracking(true);
    show();
}

//--------------------------------------------------
//
// Set functions
//
//--------------------------------------------------

void GraphDisplay::setRecordData(QList<Record> data)
{
    if (data.size() == 0)
        return;

    recordList = data;

    recordValidBegin = recordList.begin();
    recordValidEnd = recordList.end();

    QDateTime start = data.first().observationDate;
    QDateTime end = data.last().observationDate;

    setRangeToDataY();
    setDateRange(start, end);
}

void GraphDisplay::setRecordType(const Record::RecordField type)
{
    recordType = type;
    hideRegLine();

    if (recordList.empty())
        return;

    setRangeToDataY();

    repaint();
}

void GraphDisplay::setDateRange(const QDateTime timeStart,
                  const QDateTime timeEnd)
{
    // If end time is greater than start, do not update interval
    if (timeStart > timeEnd)
    {
        qDebug() << "Graph end time is greater than start time!";
        return;
    }

	// Set new range
    rangeStart = timeStart;
    rangeEnd = timeEnd;

	if (recordList.empty())
        return;

	// Clamp range to available data
	if (timeStart < recordList.first().observationDate)
		rangeStart = recordList.first().observationDate;

	if (timeEnd > recordList.last().observationDate)
		rangeEnd = recordList.last().observationDate;

    emit rangeChanged(rangeStart, rangeEnd);

    // Find records that fall in range
	updateValidRange(rangeStart, rangeEnd);

    // Calculate number of units (hours, days, etc) in the range
    int units[INTERVAL_TYPE_COUNT];
    units[INTERVAL_TYPE_HOURS] = rangeStart.secsTo(rangeEnd) / 3600;
    units[INTERVAL_TYPE_DAYS] = rangeStart.daysTo(rangeEnd);
    units[INTERVAL_TYPE_MONTHS] = units[INTERVAL_TYPE_DAYS] / 30;
    units[INTERVAL_TYPE_YEARS] = units[INTERVAL_TYPE_DAYS] /365;

    const int unitMinimum = 4;

    intervalType = INTERVAL_TYPE_HOURS;

    // Find largest unit type that has at least unitMinimum units in range
    for (int type = INTERVAL_TYPE_COUNT - 1; type > 0; type--)
    {
        if (units[type] >= unitMinimum)
        {
            intervalType = static_cast<IntervalType>(type);
            break;
        }
    }

    // Calculate unit marker spacing
    if (units[intervalType] > 0)
    {
        double minXRel = (double)(intervalXMin) / graphArea.width();

        intervalXScale = ceil(units[intervalType] * minXRel);
        intervalX = (double)(graphArea.width() * intervalXScale) / units[intervalType];
    }
    else
    {
        intervalXScale = unitMinimum;
        intervalX = intervalXMin;
    }

    // Find first date marker from the left bound of the graph
    markerStart = QDateTime();

    switch(intervalType)
    {
    case INTERVAL_TYPE_HOURS:
        markerStart.setTime(QTime(rangeStart.time().hour(), 0));
        markerStart.setDate(rangeStart.date());
        break;
    case INTERVAL_TYPE_DAYS:
        markerStart.setDate(rangeStart.date());
        markerStart = markerStart.addDays(1);
        break;
    case INTERVAL_TYPE_MONTHS:
        markerStart.setDate(QDate(rangeStart.date().year(), rangeStart.date().month(), 1));
        markerStart = markerStart.addMonths(1);
        break;
    case INTERVAL_TYPE_YEARS:
        markerStart.setDate(QDate(rangeStart.date().year() + 1, 1, 1));
        break;
    }

    qint64 timeOffset = rangeStart.secsTo(markerStart);
    intervalXOffset = graphArea.width() * (double)(timeOffset) / rangeStart.secsTo(rangeEnd);

    pixelsPerSec = (float)(graphArea.width()) / rangeStart.secsTo(rangeEnd);

    repaint();
}

void GraphDisplay::setValueRange(float start, float end)
{
    rangeYStart = start;
    rangeYEnd = end;
    rangeY = end - start;
}

void GraphDisplay::resetDateRange()
{
    if (!recordList.empty())
    {
        setDateRange(recordList.first().observationDate,
                     recordList.last().observationDate);
    }
}

//--------------------------------------------------
//
// Get functions
//
//--------------------------------------------------

QDateTime GraphDisplay::getStartDate()
{
    return rangeStart;
}

QDateTime GraphDisplay::getEndDate()
{
    return rangeEnd;
}

RegLine GraphDisplay::getRegressionLine()
{
    return regressionLine;
}

void GraphDisplay::showRegLine()
{
    if (recordList.empty())
        return;

    regressionLine.calcLinReg(recordType,
                              recordValidBegin + 1,
                              recordValidEnd - 2);

    showRegressionLine = true;
    repaint();
}

void GraphDisplay::hideRegLine()
{
    showRegressionLine = false;
    repaint();
}

//--------------------------------------------------
//
// Event functions
//
//--------------------------------------------------

void GraphDisplay::resizeEvent(QResizeEvent *event)
{
    QWidget::resizeEvent(event);

    graphArea = QRect(QPoint(marginLeft, marginTop),
                      QPoint(width() - marginRight, height() - marginBottom));

    setDateRange(rangeStart, rangeEnd);
}

void GraphDisplay::mousePressEvent(QMouseEvent * event)
{
    if (event->button() == Qt::LeftButton)
    {
        isSelectingRange = true;

        selectStart = event->x();
        selectEnd = selectStart;
    }
}

void GraphDisplay::mouseMoveEvent(QMouseEvent * event)
{
    // Update date line position to cursor
    cursorPositionX = event->x();

    // Find if mouse is hovering over data point
    Record * r = nullptr;
    float dist = getNearestDataPoint(event->x(), event->y(), &r);

    // Nearest point found
    if (r != nullptr)
    {
        // Check if point is within range of cursor
        if (dist > 0 && dist < 20)
        {
            hoverRecord = r;

            // Snap position of date line to data point
            cursorPositionX = getPosFromDate(r->observationDate);
        }
        else
            hoverRecord = nullptr;
    }

    // Update time selection range if user is holding mouse button
    if (isSelectingRange)
    {
        if (selectEnd != event->x())
            selectEnd = event->x();
    }

    repaint();
}

void GraphDisplay::mouseReleaseEvent(QMouseEvent * event)
{
    if (event->button() == Qt::LeftButton && isSelectingRange)
    {
        isSelectingRange = false;

        selectEnd = event->x();

        // Make sure end is greater than start
        if (selectEnd < selectStart)
        {
            int temp = selectEnd;
            selectEnd = selectStart;
            selectStart = temp;
        }

        // If selection is smaller than threshold, ignore
        if (selectEnd - selectStart < 5)
            return;

        // Calculate selected date range
        QDateTime start = getDateFromPos(selectStart);
        QDateTime end = getDateFromPos(selectEnd);

        // Change graph view to selected range
        if (start != end)
            setDateRange(start, end);
    }
}

void GraphDisplay::wheelEvent(QWheelEvent * event)
{
    float percentChange = (event->delta() / 1200.0f) * graphArea.width();

    QDateTime end = getDateFromPos(graphArea.width() - percentChange);
    QDateTime start = getDateFromPos(graphArea.left() + percentChange);

    setDateRange(start, end);
}

void GraphDisplay::paintEvent(QPaintEvent *)
{
    QTime drawTimer;
    drawTimer.start();

    // Initialize drawing
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);
	
    drawAxis(painter);

    if (!recordList.empty())
    {
        // Draw graph
        drawGraph(painter);

        // If user is selecting time range, show range rectangle
        if (isSelectingRange)
        {
            painter.setBrush(QBrush(QColor::fromRgb(150, 180, 200, 128)));
            painter.setPen(Qt::NoPen);

            painter.drawRect(QRect(QPoint(selectStart, graphArea.top()),
                             QPoint(selectEnd, graphArea.bottom())));
        }

        if (showRegressionLine)
            drawRegLine(painter);
    }

    // Calculate elapsed time for drawing operatons
    int elapsed = drawTimer.elapsed();

    painter.drawText(QRectF(5, 5, 42, 16),
                     Qt::AlignLeft,
                     QString::number(elapsed) + " ms");
}

//--------------------------------------------------
//
// Private get/set functions
//
//--------------------------------------------------

void GraphDisplay::setRangeToDataY()
{
    valueMin = recordList.at(0).getData(recordType);
    valueMax = recordList.at(0).getData(recordType);

    // Find min and max of data
    for (int i = 1; i < recordList.size(); i++)
    {
        float value = recordList.at(i).getData(recordType);

        if (value < valueMin)
            valueMin = value;
        else if (value > valueMax)
            valueMax = value;
    }

    float scale = (valueMax - valueMin) * scalingMarginY;

    // Set range to data min and max
    setValueRange(valueMin, valueMax + scale);
}

float GraphDisplay::getValueFromPos(int y) const
{
    return (1.0f - (float)(y) / graphArea.height()) * rangeY + rangeYStart;
}

int GraphDisplay::getPosFromValue(float value) const
{
	return graphArea.height() * (1.0f - ((value - rangeYStart) / rangeY)) + graphArea.top();
}

QDateTime GraphDisplay::getDateFromPos(int x) const
{
    qint64 dateRange = rangeStart.secsTo(rangeEnd);
    qint64 secondsPerPixel = dateRange / graphArea.width();
    qint64 fromStart = secondsPerPixel * (x - graphArea.left());

    return rangeStart.addSecs(fromStart);
}

int GraphDisplay::getPosFromDate(const QDateTime date) const
{
    return pixelsPerSec * rangeStart.secsTo(date) + graphArea.left();
}

float GraphDisplay::getNearestDataPoint(int x, int y, Record ** r) const
{
    if (recordList.empty())
        return -1;

    // Create test record with date corresponding to x position to compare
    // with record list
    Record testDate;
    testDate.observationDate = getDateFromPos(x);

    // Find records that the point lies between

    // qLowerBound finds the record corresponding to the first date larger
    // than that of the test record using a binary search
    auto rightIterator = qLowerBound(recordValidBegin, recordValidEnd, testDate, compareRecord);
    auto leftIterator = rightIterator - 1;

    // Find the nearest of the two records
    if (rightIterator == recordList.begin())
    {
        // Left record is invalid, use right record
        *r = &(*rightIterator);
    }
    else if (rightIterator == recordList.end())
    {
        // Right record is invalid, use left
        *r = &(*leftIterator);
    }
    else
    {
        // Compare distances of each record
        Record * recLeft = &(*leftIterator);
        Record * recRight = &(*rightIterator);

        float distLX = getPosFromDate(recLeft->observationDate) - x;
        float distRX = getPosFromDate(recRight->observationDate) - x;
        float distLY = getPosFromValue(recLeft->getData(recordType)) - y;
        float distRY = getPosFromValue(recRight->getData(recordType)) - y;

        float distL = distLX * distLX + distLY * distLY;
        float distR = distRX * distRX + distRY * distRY;

        // Find record closest to point
        if (distL < distR)
        {
            *r = recLeft;
            return sqrt(distL);
        }
        else
        {
            *r = recRight;
            return sqrt(distR);
        }
    }

    float deltaX = getPosFromDate((*r)->observationDate) - x;
    float deltaY = getPosFromValue((*r)->getData(recordType)) - y;

    return sqrt(deltaX * deltaX + deltaY * deltaY);
}

//--------------------------------------------------
//
// Utility functions
//
//--------------------------------------------------

void GraphDisplay::updateValidRange(const QDateTime start, const QDateTime end)
{
    // Create dummy records to compare with
    Record recStart; recStart.observationDate = start;
    Record recEnd; recEnd.observationDate = end;

    // Find first and last records in graph display range
    recordValidBegin = qLowerBound(recordList.begin(), recordList.end(),
                                   recStart, compareRecord);
    recordValidEnd = qLowerBound(recordList.begin(), recordList.end(),
                                 recEnd, compareRecord);

    // Include first records off screen to left and right
    if (recordValidBegin > recordList.begin())
        --recordValidBegin;
    if (recordValidEnd < recordList.end())
        ++recordValidEnd;
}

//--------------------------------------------------
//
// Graph drawing functions
//
//--------------------------------------------------

void GraphDisplay::drawAxis(QPainter &painter)
{
    // Y interval lines
    painter.setBrush(Qt::NoBrush);
    painter.setPen(gridHorizPen);
    painter.setFont(QFont("Helvetica"));

    QString unitString = Record::getUnitString(recordType);

    for (int y = graphArea.top(); y < graphArea.bottom(); y += intervalY)
    {
        painter.drawLine(graphArea.left(), y, graphArea.right(), y);
        painter.drawText(QRectF(graphArea.right() - 208, y + 4, 200, 16),
                         Qt::AlignRight,
                         QString::number(getValueFromPos(y), 'f', 1) +
                         " " + unitString);
    }

	// Draw current date line
	painter.drawLine(cursorPositionX, graphArea.bottom(), cursorPositionX, graphArea.top());
    painter.drawText(QRectF(cursorPositionX + 2, graphArea.center().y(), 200, 16),
                         Qt::AlignLeft, getDateFromPos(cursorPositionX).toString());

    // X interval markers
    painter.setBrush(Qt::NoBrush);
    for (int x = graphArea.left(); x < graphArea.right(); x += intervalX)
    {
        painter.setPen(gridVertPen);
        painter.drawLine(x, graphArea.top(), x, graphArea.bottom());

        painter.setPen(gridIntervalPen);
        painter.drawLine(x, graphArea.bottom() - 5, x, graphArea.bottom());
    }

    // X axis label area
    painter.setPen(Qt::NoPen);
    painter.setBrush(bottomBoxBrush);
    painter.drawRect(graphArea.left(), graphArea.bottom(),
                     graphArea.right(), height());

    // X axis labels
    painter.setBrush(Qt::black);
    painter.setPen(Qt::black);

    QDateTime currentTime = markerStart;

    for (int x = graphArea.left() + intervalXOffset; x < graphArea.right(); x += intervalX)
    {
        QString timeStr;
        if (intervalType == INTERVAL_TYPE_HOURS)
        {
            timeStr = currentTime.toString("h ap");
            currentTime = currentTime.addSecs(intervalXScale * 3600);
        }
        else if (intervalType == INTERVAL_TYPE_DAYS)
        {
            timeStr = currentTime.toString("ddd d");
            currentTime = currentTime.addDays(intervalXScale);
        }
        else if (intervalType == INTERVAL_TYPE_MONTHS)
        {
            timeStr = currentTime.toString("MMM");
            currentTime = currentTime.addMonths(intervalXScale);
        }
        else if (intervalType == INTERVAL_TYPE_YEARS)
        {
            timeStr = currentTime.toString("yyyy");
            currentTime = currentTime.addYears(intervalXScale);
        }

        painter.drawText(QRectF(x, graphArea.bottom() + 2, intervalX, marginBottom),
                         Qt::AlignTop, timeStr);
    }
}

void GraphDisplay::drawGraph(QPainter &painter)
{
    QPainterPath path;

    painter.setBrush(Qt::NoBrush);
    painter.setPen(graphPen);

    int x = 0;
    for (auto iter = recordValidBegin; iter < recordValidEnd; iter++)
    {
        Record r = *iter;

        x = getPosFromDate(r.observationDate);
        int y = getPosFromValue(r.getData(recordType));

        // Set first point
        if (iter == recordValidBegin)
            path.moveTo(x, graphArea.bottom());

        // Add line to graph
        path.lineTo(x, y);

        // Draw graph data point
        if (hoverRecord != nullptr && hoverRecord->recordId == r.recordId)
        {
            painter.setBrush(graphBrush);
            painter.drawEllipse(QPoint(x, y), 4, 4);
            painter.setBrush(Qt::NoBrush);
        }
        else
            painter.drawEllipse(QPoint(x, y), 3, 3);
    }

    // Draw final graph point
    path.lineTo(x, graphArea.bottom());
    path.closeSubpath();

    // Draw graph
	// NOTE: The graph is drawn twice, first is the shaded area with antialiasing
	//        disabled, and the second is just the line with antialiasing on.
	//        This is a workaround for performance issues with fills and 
	//        antialiasing (performance increase of around 7000%)

	// Draw shaded graph area
	painter.setRenderHint(QPainter::Antialiasing, false);
    painter.setBrush(graphBrush);
	painter.setPen(Qt::NoPen);
    painter.drawPath(path);

	// Draw graph line
	painter.setRenderHint(QPainter::Antialiasing, true);
	painter.setBrush(Qt::NoBrush);
	painter.setPen(graphPen);
    painter.drawPath(path);

    // Draw hovered over data point info box
    if (hoverRecord != nullptr)
    {
        int x = getPosFromDate(hoverRecord->observationDate);
        int y = getPosFromValue(hoverRecord->getData(recordType));

        painter.setBrush(QBrush(QColor::fromRgb(250, 250, 250, 200)));
        painter.setPen(Qt::NoPen);

        painter.drawRect(QRect(x - 3, y + 6, 150, 36));

        painter.setPen(Qt::black);

        painter.drawText(QRect(x, y + 6, 150, 16), Qt::AlignTop | Qt::AlignLeft,
                         hoverRecord->observationDate.toString());
        painter.drawText(QRect(x, y + 22, 150, 16), Qt::AlignTop| Qt::AlignLeft,
                         QString::number(hoverRecord->getData(recordType)) + " "
                         + Record::getUnitString(recordType) );
    }
}

void GraphDisplay::drawRegLine(QPainter &painter)
{
    // Find where line crosses left and right edges of graph
    float leftInt = regressionLine.getY(rangeStart);
    float rightInt = regressionLine.getY(rangeEnd);
    float endInt = regressionLine.getY(regressionLine.endTime);

    QPoint leftPoint(graphArea.left(), getPosFromValue(leftInt));
    QPoint startPoint(getPosFromDate(regressionLine.startTime),
                      getPosFromValue(regressionLine.intercept));
    QPoint endPoint(getPosFromDate(regressionLine.endTime),
                    getPosFromValue(endInt));
    QPoint rightPoint(graphArea.right(), getPosFromValue(rightInt));

    // Draw solid line in range of data points used to calculate linear fit
    painter.setBrush(Qt::NoBrush);
    painter.setPen(QPen(QBrush(QColor::fromRgb(80,40,20)), 2));
    painter.drawLine(QLine(startPoint, endPoint));

    // Draw dotted line outside of linear fit range
    painter.setPen(QPen(QBrush(QColor::fromRgb(80,70,40)), 1, Qt::DashLine));
    painter.drawLine(QLine(leftPoint, startPoint));
    painter.drawLine(QLine(endPoint, rightPoint));

}

bool GraphDisplay::compareRecord(const Record &r1, const Record &r2)
{
    return r1.observationDate < r2.observationDate;
}
